/* 
 * TRAXIS Publisher intercepting proxy server by wouterlucas.com
 *
 * Copyright (c) 2012 Wouter-lucas van Boesschoten
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 *   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
var util = require('util');
var url = require('url');
var fs = require('fs');

//settings
var traxisIp = 'ec2-184-169-199-155.us-west-1.compute.amazonaws.com';
var traxisPort = '80';
var tribuneXml = 'test_lineup.xml';
var proxyPort = '9090';

//initialize our logger, using Winston (http://toolbox.no.de/packages/winston) which supports remote logging if required
var winston = require('winston');
//custom winston log levels
var customLevels = {
  levels: {
    debug: 0,
    info: 1,
    warning: 2,
    error: 3
  },
  colors: {
    debug: 'cyan',
    info: 'green',
    warning: 'yellow',
    error: 'red'
  }
}

// create the logger
var logger = module.exports = new (winston.Logger)({
  level: 'debug',
  levels: customLevels.levels,
  handleExceptions: true,
  transports: [
    // setup console logging
    new (winston.transports.File)({ 
    	filename: 'traxisproxy.log',
    	level: 'info',
    	levels: customLevels.levels,
    	handleExceptions: true
    }),
    new (winston.transports.Console)({
		level: 'info',
		levels: customLevels.levels,
		handleExceptions: true,
		colorize: true,
		timestamp: true
    })
  ]
})

// set the coloring
winston.addColors(customLevels.colors)

//initialize our HTTP agents & pools
var http = require('http');

//xml parser, using a Node JS implementation of ElementTree based on Python's ElementTree parser
var et = require('elementtree');
var XML = et.XML;
var ElementTree = et.ElementTree;
var element = et.Element;
var subElement = et.SubElement;


//our in memory array's
var RegionalChannelLineup = {};
var RegionList = new Array();

/*RegionalChannelLineup =
 * [
 *	1: [{"id": "channel1", "lcn": "1"},{"id": "channel2", "lcn": "2"}],
 *	2: [{id: "channel1", lcn: "1"}],
 *	3: [{id: "channel1", lcn: "1"}]
 *	]
 */

//program starts here
function regionInRegionalChannelLineup(regionId){
	//check if we know about the region
	if (RegionList.indexOf(regionId) != -1){
		return true
	}
	return false
}

function getLcnForChannelByRegionId(channelId, region){
	//resolves region to a channel and returns it's lcn (if available)
	//return -1 if no LCN is found for the channel in the region
	if (RegionalChannelLineup[region] != undefined){
		for (var z=0; z<RegionalChannelLineup[region].length; z++){
			if (RegionalChannelLineup[region][z].id == channelId) {
				return RegionalChannelLineup[region][z].lcn
			}
		}
	}
	
	return -1
}

function filterChannelByRegionId(channelId, region) {
	//resolves the region to a channel by identifier in the in memory arrays
	//if the channel is part of the region return True
	//if the channel is not part of the region return False
	if (RegionalChannelLineup[region] != undefined){
		for (var z=0; z<RegionalChannelLineup[region].length; z++){
			if (RegionalChannelLineup[region][z].id == channelId) {
				return true
			}
		}
	}
	//return false if we got nothing
	return false
}

function numOrdA(a, b){ return (a-b); }
function numOrdD(a, b){ return (b-a); }

//start our webserver & proxy service
http.createServer(function(clientrequest, clientresponse) {
  	var isJsonRequest = false; //is used to determine whether we are decrypting a json or xml response
  	var isChannelRequest = false; //is used to determine whether we need to do anything or not
  	var hasRegion = false; //is used to determine whether the client is requesting for regionalized information or not
  	var regionId = 0; //found region id as part of the url
  	var responseBuffer = [];
  	var requestIsOk = true;
  	var sort = false;
  	var traxisResponseHeaders;
  	
  	//Request checkers
  	//Check in the client url if it is asking for channels! If it aint, just skip the whole processing and stream the response back
  	if (clientrequest.url.slice(0, 20).toLowerCase() == '/traxis/web/channels'){
  		isChannelRequest = true;
  	}
  	
  	//cast the uri to lower case
  	uri = url.parse(clientrequest.url.toLowerCase(), true);
	
	//see if it has a regionid
	if ((typeof clientrequest.headers['x-region'] != 'undefined') || (typeof uri.query.regionid != 'undefined')) {
		hasRegion = true;
		//regionId = uri.query.regionid
		if (typeof clientrequest.headers['x-region'] != 'undefined') { regionId = clientrequest.headers['x-region'].toLowerCase(); }
		if (typeof uri.query.regionid != 'undefined') { regionId = uri.query.regionid.toLowerCase(); }
		logger.debug('Request includes a regionId: ' + regionId);
		if (regionInRegionalChannelLineup(regionId) == false) {
			//region does not exist in our array, return 400 bad request
	  		logger.error('Error regionid: ' + regionId + ' not found');
	  		clientresponse.writeHead(400);
	  		clientresponse.end();
	  		requestIsOk=false;				
		}
	}
	
	//see if the request is asking to sort anything by logical channel number
	var sortPatt=new RegExp("/sort/logicalchannelnumber|/sort/logicalchannelnumber,name|/sort/name,logicalchannelnumber");
	if (sortPatt.test(clientrequest.url.toLowerCase()) == true) {
		sort = true;
	}

	//see if it has a json output parameter in the request
	if (typeof uri.query.output !='undefined') {
		if (uri.query.output == 'json') {
			isJsonRequest = true;
			logger.debug('Client is requesting for a JSON response')
		}
	}
	
	logger.info("Client request: " + clientrequest.connection.remoteAddress + ': ' + clientrequest.method + ' ' + clientrequest.url +  ' - isChannelsRequest=' + isChannelRequest + ' - hasRegion=' + hasRegion + ' - regionId=' + regionId + ' - isJsonRequest=' + isJsonRequest + ' - sort=' + sort);
	
	//Forward the request to TRAXIS
	//set the options to the http.request handler
	//DO NOT USE the Node JS HTTP Pooling Agent. You will run into socket errors (NOBUFS).
	var options = {
  		host: traxisIp,
  		port: traxisPort,
  		path: clientrequest.url,
  		method: clientrequest.method,
  		headers: clientrequest.headers,
  		agent: false
	};
	
	//Forward the request to TRAXIS
	if (requestIsOk == true) {
		var proxyrequest = http.request(options, function(res) {
			logger.debug('writing headers back to client statuscode: ' + res.statusCode + ' headers: ' + res.headers);
			//if it not a 200, make sure we dont parse anything by setting the isChannelRequest to false
			if ((res.statusCode != 200) || (clientrequest.method == 'OPTIONS')) 
				{ isChannelRequest=false; }
			
			//only write our headers if we are not intercepting it
			if ((hasRegion == false) || (isChannelRequest == false)) {
				//write our headers to the client
				clientresponse.writeHead(res.statusCode, res.headers);
			} else {
				//save our headers
				traxisResponseHeaders = res.headers;
			}
			
			logger.info("TRAXIS Response: " + res.connection.remoteAddress + ': HTTP/' + res.httpVersion + ' ' + res.statusCode);
		});
	
	  	
	  	//check for errors
	  	proxyrequest.on('error', function(e) {
	  		logger.error('Error problem with request to TRAXIS: ' + e.message);
	  		responseHeaders = {
        		'Server': 'Node v' + process.versions.node + ' Chrome V8 v' + process.versions.v8, 
				'Connection': 'close'
        	}
	  		clientresponse.writeHead(500, responseHeaders);
	  		clientresponse.end("Error: " + e.message);
	  		requestIsOk=false;
		});
	  	
		//Listen for original request BODY chunks
		clientrequest.on('data', function(clientchunk) {
			logger.debug('got clientrequest chunk sending to TRAXIS. Chunk: ' + clientchunk);
			proxyrequest.write(clientchunk, 'binary');
	  	});
	  	//listen for original end of message, close traxis connection with it
	  	clientrequest.on('end', function() {
			logger.debug('got end from client, closing traxis connection');
			proxyrequest.end();
	  	});
	  	  	
		//Listen for TRAXIS response messages
		proxyrequest.on('response', function (traxisresponse) {
			logger.debug('got response from traxis');
			//if it is a data chunk, forward or buffer it
	  		traxisresponse.on('data', function (traxischunk) {
	  			//if (debug == true) { util.debug('chunk ' + traxischunk) };
	    		if ((requestIsOk == true ) && (hasRegion == true) && (isChannelRequest == true)) {
	      			//logger.debug('writing to buffer');
	      			responseBuffer += traxischunk;
	      		} else {
					//logger.debug('writing back to client');
					clientresponse.write(traxischunk);
	      		}
	  		});
	  		
	  		//Listen for TRAXIS response end, send the stream and close the connection
			traxisresponse.on('end', function (traxisresponse) {	
				logger.debug('got end from traxis');	
		      	if ((hasRegion == true) && (isChannelRequest == true) && (responseBuffer.length>0)) {
					if (isJsonRequest == true) {
		      			logger.debug('parsing json response from traxis');
		      			parseJson(responseBuffer, regionId, sort, ParsedJsonResponseCallBack);
		    		} else if (isJsonRequest == false) {
		    			logger.debug('parsing xml response from traxis');
		     			parseXml(responseBuffer, regionId, sort, ParsedXmlResponseCallBack);
		      		}

				} else {
					//tidy up
					logger.info('Got end from Traxis, proxying request back to client');
					clientresponse.end();
				}
			});
		});
		
		//our parsed call back handlers
		//write the head with our headers and the 200 status code. Generate the content length on the fly
		function ParsedXmlResponseCallBack(data){
        	logger.info('Finished parsing xml response, writing headers and content back to client');
        	//write our headers
        	if ((traxisResponseHeaders != undefined) || (traxisResponseHeaders != "")) {
        		traxisResponseHeaders['content-length'] = data.length;
        		traxisResponseHeaders['server'] = 'Node v' + process.versions.node + ' Chrome V8 v' + process.versions.v8;
        	} else {
        		traxisResponseHeaders = {
	        		'Content-Length': data.length,
	        		'Server': 'Node v' + process.versions.node + ' Chrome V8 v' + process.versions.v8,
					'Content-Type': 'text/xml',
					'Connection': 'close'
					}
        	}
        	
        	clientresponse.writeHead(200, traxisResponseHeaders);
        	clientresponse.end(data, 'ascii');
    	}
    	
    	//out parsed XML call back handler
    	//write the head with out headers and the 200 status code. Generate the content length on the fly
    	function ParsedJsonResponseCallBack(data){
        	logger.info('Finished parsing json response, writing headers and content back to client');
        	//write our headers
        	if ((traxisResponseHeaders != undefined) || (traxisResponseHeaders != "")) {
        		traxisResponseHeaders['content-length'] = data.length;
        		traxisResponseHeaders['server'] = 'Node v' + process.versions.node + ' Chrome V8 v' + process.versions.v8;
        	} else {
        		traxisResponseHeaders = {
        		'Content-Length': data.length,
        		'Server': 'Node v' + process.versions.node + ' Chrome V8 v' + process.versions.v8,
				'Content-Type': 'application/json',
				'Connection': 'close'
        		};
        	}
        	
        	clientresponse.writeHead(200, traxisResponseHeaders);
        	clientresponse.end(data, 'ascii');
    	}
		
	}
	
	
	function parseXml(xml, region, sort, callback){
		//parses the TRAXIS XML responses and looks for <Channel> objects. 
		doc = et.parse(xml);
		
		//get channels
		channels = doc.find('./');
		var channelList = doc.findall('./Channel');
		var toDelete = new Array();
		var lcnList = new Array();
		var channelsInXml = 0;
		
		newXmlDoc = et.parse('<Channels xmlns="urn:eventis:traxisweb:1.0"></Channels>');
		//newXmlDoc.clear()
		newChannels = newXmlDoc.find('./');
		
		if (channelList.length == 0) { callback('') };
		
		channelList = channels.getchildren();
		
		for (var j=0; j<RegionalChannelLineup[region].length; j++) {
			var id = RegionalChannelLineup[region][j].id;
			
			for (var k=0; k<channelList.length; k++) {				
				if (id == channelList[k].get('id')) {
					if (channelList[k].find('LogicalChannelNumber') != undefined) {
						channelList[k].find('LogicalChannelNumber').text = RegionalChannelLineup[region][j].lcn;
					} else {
						channelList[k].append(element('LogicalChannelNumber'));
						channelList[k].find('./LogicalChannelNumber').text = RegionalChannelLineup[region][j].lcn;				
					}
					newChannels.append(channelList[k]);
					break;			
				}
			}	
		}
		
		//add resultCount
		if (typeof channels !='undefined') {
			newChannels.attrib['resultCount'] = newChannels.findall('./Channel').length;
		}
		
		var s = newXmlDoc.write();
		callback(s);
		

	}
	
	function parseJson(json, region, sort, callback){
		//parses the TRAXIS JSON responses and looks for Channel objects	
		var parsedJSON = eval('('+json+')');
		var toDelete = new Array();
		var channelsInJson = 0;
		var lcnList = new Array();
		
		newJsonObj = '{"Channels":{"resultCount":0,"Channel": []}}';
		var newParsedJsonObj = eval('('+newJsonObj+')');
		
		if (parsedJSON.Channels.Channel == undefined) { callback('') };
		
		for (var j=0; j<RegionalChannelLineup[region].length; j++) {
			for (var k=0; k<parsedJSON.Channels.Channel.length; k++) {	
				if (parsedJSON.Channels.Channel[k].id == RegionalChannelLineup[region][j].id) {
					parsedJSON.Channels.Channel[k].LogicalChannelNumber = RegionalChannelLineup[region][j].lcn;
					newParsedJsonObj.Channels.Channel.push(parsedJSON.Channels.Channel[k]);
					break;
				}
			}
		}
		
		//add resultCount
		if (typeof parsedJSON.Channels !='undefined') {
			newParsedJsonObj.Channels.resultCount = newParsedJsonObj.Channels.Channel.length;
		};
		
		logger.info('Parsed json intercepted resonse, total amount channels in Traxis response: ' + parsedJSON.Channels.Channel.length  + ' found: ' + newParsedJsonObj.Channels.resultCount + ' channels that are valid in region: ' + region);
		
		var s = JSON.stringify(newParsedJsonObj);
		callback(s);
	}
		
}).listen(proxyPort);

logger.info('Started HTTP service.');

logger.info('Parsing ' + tribuneXml + '...');
//start reading the Tribune XML file asynchronously and callback when done
fs.readFile(tribuneXml, 'ascii', function (err, xmlData) {
  	if (err) throw err;
  
  	tribune = et.parse(xmlData); 
	var headends = tribune.findall('./lineups/headend');
  	var totalChannels = 0;
  	var totalRegions = headends.length;
  
  	logger.debug('Headends length:' + headends.length);
  	for (var l=0; l<headends.length; l++) {
  		//get the regionid
  		var regionId = headends[l].get('headendId');
  		logger.debug('found regionid: ' + regionId);
  		//get the lineup
  		var lineups = headends[l].findall('lineup');
  		for (var n=0; n<lineups.length; n++) {
  			//add the region specific device linup permutation
  			var lineupRegionId = regionId;
  			var lineupDeviceAttr = lineups[n].get('device');
  			logger.debug('lineup device value: ' + lineupDeviceAttr);
  			if (lineupDeviceAttr != undefined) {
  				//we have a device attribute, add the additional region granularity by appending the device attribute
				lineupRegionId = regionId + "-" + lineupDeviceAttr;
  				}
		  	logger.debug('getting channels');
			//get the channellineup	
	  		stations = lineups[n].findall('station');
	  		//add the regionId to the RegionList array for fast lookup
	  		RegionList.push(lineupRegionId.toLowerCase());
	  		//add to our internal object for comprehensive processing
	  		RegionalChannelLineup[lineupRegionId.toLowerCase()] = [];
	  		for (var m=0; m<stations.length; m++){
	  			//for each station in the headend get the channelId
	  			var channelId = stations[m].get('prgSvcId');
	  			var lcn = stations[m].find('chan').text;
	  			lcn = parseInt(lcn,10)
	  			logger.debug('adding channelid: ' + channelId + ' with lcn: ' + lcn + ' for region: ' + lineupRegionId);
	  			//add the channelId to the RegionChannelList per region tuple
	  			RegionalChannelLineup[lineupRegionId.toLowerCase()].push(
	  				{"id":channelId,"lcn":lcn}
	  			);
	  			totalChannels++;
  			}
  		}
  	}
  	logger.info('Successfully rad the Tribune XML file: ' + tribuneXml + ' total regions processed: ' + totalRegions + ' total regionalized channels processed: ' + totalChannels);
});

logger.info('Listening for incomming connections.');